/*
函数参数的默认值
检查函数log的参数y有没有赋值，
如果没有，则指定默认值为World。这种写法的缺点在于，
如果参数y赋值了，但是对应的布尔值为false，则该赋值不起作用。就像上面代码的最后一行，参数y等于空字符，结果被改为默认值。
*/
// function log(x, y) {
//   y = y || 'World';
//   console.log(x, y);
// }

// log('Hello') // Hello World
// log('Hello', 'China') // Hello China
// log('Hello', '') // Hello World
/*
 允许为函数的参数设置默认值，即直接写在参数定义的后面。
 可以看到，ES6 的写法比 ES5 简洁许多，而且非常自然
*/
// function log(x, y = 'World') {
//   console.log(x, y);
// }

/*
参数变量是默认声明的，所以不能用let或const再次声明
参数变量x是默认声明的，在函数体中，不能用let或const再次声明，否则会报错。
*/
// function foo(x = 5) {
//   let x = 1; // error
//   const x = 2; // error
// }

/*
使用参数默认值时，函数不能有同名参数。
一个容易忽略的地方是，参数默认值不是传值的，而是每次都重新计算默认值表达式的值。
也就是说，参数默认值是惰性求值的。
*/
// // 不报错
// function foo(x, x, y) {
//   // ...
// }

// // 报错
// function foo(x, x, y = 1) {
//   // ...
// }

/*
参数p的默认值是x + 1。这时，每次调用函数foo，都会重新计算x + 1，而不是默认p等于 100。
*/
// let x = 99;
// function foo(p = x + 1) {
//   console.log(p);
// }

// foo() // 100

// x = 100;
// foo() // 101

/*
与解构赋值默认值结合使用
参数默认值可以与解构赋值的默认值，结合起来使用。

代码只使用了对象的解构赋值默认值，没有使用函数参数的默认值。
只有当函数foo的参数是一个对象时，变量x和y才会通过解构赋值生成。
如果函数foo调用时没提供参数，变量x和y就不会生成，从而报错。
通过提供函数参数的默认值，就可以避免这种情况。
*/

// function foo({x, y = 5}) {
//   console.log(x, y);
// }

// foo({}) // undefined 5
// foo({x: 1}) // 1 5
// foo({x: 1, y: 2}) // 1 2

// foo() // TypeError: Cannot read property 'x' of undefined

/*
如果没有提供参数，函数foo的参数默认为一个空对象。
*/
// function foo({x, y = 5} = {}) {
//   console.log(x, y);
// }

// foo() // undefined 5

/*
如果函数fetch的第二个参数是一个对象，就可以为它的三个属性设置默认值。
这种写法不能省略第二个参数，如果结合函数参数的默认值，就可以省略第二个参数。这时，就出现了双重默认值。
*/
// function fetch(url, { body = '', method = 'GET', headers = {} }) {
//   console.log(method);
// }

// fetch('http://example.com', {})
// // "GET"

// fetch('http://example.com')
// // 

/*
参数默认值的位置 
通常情况下，定义了默认值的参数，应该是函数的尾参数。
因为这样比较容易看出来，到底省略了哪些参数。如果非尾部的参数设置默认值，实际上这个参数是没法省略的。

有默认值的参数都不是尾参数。这时，无法只省略该参数，而不省略它后面的参数，除非显式输入undefined。
*/

// // 例一
// function f(x = 1, y) {
//   return [x, y];
// }

// f() // [1, undefined]
// f(2) // [2, undefined])
// f(, 1) // 报错
// f(undefined, 1) // [1, 1]

// // 例二
// function f(x, y = 5, z) {
//   return [x, y, z];
// }

// f() // [undefined, 5, undefined]
// f(1) // [1, 5, undefined]
// f(1, ,2) // 报错
// f(1, undefined, 2) // [1, 5, 2]


/*
如果传入undefined，将触发该参数等于默认值，null则没有这个效果。

x参数对应undefined，结果触发了默认值，y参数等于null，就没有触发默认值。
*/

// function foo(x = 5, y = 6) {
//   console.log(x, y);
// }

// foo(undefined, null)
// 5 null

/*
函数的 length 属性 
指定了默认值以后，函数的length属性，将返回没有指定默认值的参数个数。也就是说，指定了默认值后，length属性将失真。

length属性的返回值，等于函数的参数个数减去指定了默认值的参数个数。
比如，上面最后一个函数，定义了 3 个参数，其中有一个参数c指定了默认值，因此length属性等于3减去1，最后得到2。
*/
 
// (function (a) {}).length // 1
// (function (a = 5) {}).length // 0
// (function (a, b, c = 5) {}).length // 2

/*
设置了默认值的参数不是尾参数，那么length属性也不再计入后面的参数了。
*/
// (function (a = 0, b, c) {}).length // 0
// (function (a, b = 1, c) {}).length // 1

/*
作用域
一旦设置了参数的默认值，函数进行声明初始化时，参数会形成一个单独的作用域（context）。
等到初始化结束，这个作用域就会消失。这种语法行为，在不设置参数默认值时，是不会出现的。

参数y的默认值等于变量x。调用函数f时，参数形成一个单独的作用域。
在这个作用域里面，默认值变量x指向第一个参数x，而不是全局变量x，所以输出是2。
*/
// var x = 1;

// function f(x, y = x) {
//   console.log(y);
// }

// f(2) //2


/*
函数f调用时，参数y = x形成一个单独的作用域。这个作用域里面，变量x本身没有定义，所以指向外层的全局变量x。函数调用时，函数体内部的局部变量x影响不到默认值变量x
*/
// let x = 1;

// function f(y = x) {
//   let x = 2;
//   console.log(y);
// }

// f()//1


/*
，全局变量x不存在，就会报错。
*/
// function f(y = x) {
//   let x = 2;
//   console.log(y);
// }

// f() // ReferenceError: x is not defined

/*
如果参数的默认值是一个函数，该函数的作用域也遵守这个规则。
函数bar的参数func的默认值是一个匿名函数，返回值为变量foo。
函数参数形成的单独作用域里面，并没有定义变量foo，所以foo指向外层的全局变量foo，因此输出outer。
*/
// let foo = 'outer';

// function bar(func = () => foo) {
//   let foo = 'inner';
//   console.log(func());
// }

// bar(); // outer


/*
rest 参数 
ES6 引入 rest 参数（形式为...变量名），用于获取函数的多余参数，这样就不需要使用arguments对象了。
rest 参数搭配的变量是一个数组，该变量将多余的参数放入数组中。

代码的add函数是一个求和函数，利用 rest 参数，可以向该函数传入任意数目的参数。
*/

// function add(...values) {
//   let sum = 0;

//   for (var val of values) {
//     sum += val;
//   }

//   return sum;
// }

// add(2, 5, 3) // 10

/*
一个 rest 参数代替arguments变量的例子。

arguments对象不是数组，而是一个类似数组的对象。
所以为了使用数组的方法，必须使用Array.prototype.slice.call先将其转为数组。
rest 参数就不存在这个问题，它就是一个真正的数组，数组特有的方法都可以使用。
*/
// // arguments变量的写法
// function sortNumbers() {
//   return Array.prototype.slice.call(arguments).sort();
// }

// // rest参数的写法
// const sortNumbers = (...numbers) => numbers.sort();

// sortNumbers(1,5,3)


/*
箭头函数 

ES6 允许使用“箭头”（=>）定义函数。
*/
// var f = v => v;

// // 等同于
// var f = function (v) {
//   return v;
// };

/*
如果箭头函数不需要参数或需要多个参数，就使用一个圆括号代表参数部分。
*/

// var f = () => 5;
// // 等同于
// var f = function () { return 5 };

// var sum = (num1, num2) => num1 + num2;
// // 等同于
// var sum = function(num1, num2) {
//   return num1 + num2;
// };

/*
如果箭头函数的代码块部分多于一条语句，就要使用大括号将它们括起来，并且使用return语句返回。
*/
// var sum = (num1, num2) => { return num1 + num2; }
/*
由于大括号被解释为代码块，所以如果箭头函数直接返回一个对象，必须在对象外面加上括号，否则会报错。
*/ 
// // 报错
// let getTempItem = id => { id: id, name: "Temp" };

// // 不报错
// let getTempItem = id => ({ id: id, name: "Temp" });

/*
下面是一种特殊情况，虽然可以运行，但会得到错误的结果。

原始意图是返回一个对象{ a: 1 }，但是由于引擎认为大括号是代码块，所以执行了一行语句a: 1。
这时，a可以被解释为语句的标签，因此实际执行的语句是1;，然后函数就结束了，没有返回值。
*/
// let foo = () => { a: 1 };
// foo() // undefined


/*
如果箭头函数只有一行语句，且不需要返回值，可以采用下面的写法，就不用写大括号了。
*/
// let fn = () => void doesNotReturn();

/*
箭头函数可以与变量解构结合使用。
*/
// const full = ({ first, last }) => first + ' ' + last;

// // 等同于
// function full(person) {
//   return person.first + ' ' + person.last;
// }


/*
箭头函数使得表达更加简洁
代码只用了两行，就定义了两个简单的工具函数。
如果不用箭头函数，可能就要占用多行，而且还不如现在这样写醒目。
*/
// const isEven = n => n % 2 === 0;
// const square = n => n * n;

/*
箭头函数的一个用处是简化回调函数。
*/
// // 正常函数写法
// [1,2,3].map(function (x) {
//   return x * x;
// });

// // 箭头函数写法
// [1,2,3].map(x => x * x);


// // 正常函数写法
// var result = values.sort(function (a, b) {
//   return a - b;
// });

// // 箭头函数写法
// var result = values.sort((a, b) => a - b);

/*
rest 参数与箭头函数结合的例子。
*/
// const numbers = (...nums) => nums;

// numbers(1, 2, 3, 4, 5)
// // [1,2,3,4,5]

// const headAndTail = (head, ...tail) => [head, tail];

// headAndTail(1, 2, 3, 4, 5)
// // [1,[2,3,4,5]]


/*
箭头函数有几个使用注意点。

（1）函数体内的this对象，就是定义时所在的对象，而不是使用时所在的对象。

（2）不可以当作构造函数，也就是说，不可以使用new命令，否则会抛出一个错误。

（3）不可以使用arguments对象，该对象在函数体内不存在。如果要用，可以用 rest 参数代替。

（4）不可以使用yield命令，因此箭头函数不能用作 Generator 函数。

上面四点中，第一点尤其值得注意。this对象的指向是可变的，但是在箭头函数中，它是固定的。



setTimeout的参数是一个箭头函数，这个箭头函数的定义生效是在foo函数生成时，而它的真正执行要等到 100 毫秒后。
如果是普通函数，执行时this应该指向全局对象window，这时应该输出21。
但是，箭头函数导致this总是指向函数定义生效时所在的对象（本例是{id: 42}），所以输出的是42。
*/
// function foo() {
//   setTimeout(() => {
//     console.log('id:', this.id);
//   }, 100);
// }

// var id = 21;

// foo.call({ id: 42 });
// // id: 42


/*
箭头函数可以让setTimeout里面的this，绑定定义时所在的作用域，而不是指向运行时所在的作用域。

Timer函数内部设置了两个定时器，分别使用了箭头函数和普通函数。
前者的this绑定定义时所在的作用域（即Timer函数），后者的this指向运行时所在的作用域（即全局对象）。
所以，3100 毫秒之后，timer.s1被更新了 3 次，而timer.s2一次都没更新。
*/
// function Timer() {
//   this.s1 = 0;
//   this.s2 = 0;
//   // 箭头函数
//   setInterval(() => this.s1++, 1000);
//   // 普通函数
//   setInterval(function () {
//     this.s2++;
//   }, 1000);
// }

// var timer = new Timer();

// setTimeout(() => console.log('s1: ', timer.s1), 3100);
// setTimeout(() => console.log('s2: ', timer.s2), 3100);
// // s1: 3
// // s2: 0



















